package com.chl.tools;

import java.io.BufferedInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.apache.poi.hwpf.HWPFDocument;
import org.apache.poi.hwpf.extractor.WordExtractor;
import org.apache.poi.hwpf.model.PicturesTable;
import org.apache.poi.hwpf.usermodel.CharacterRun;
import org.apache.poi.hwpf.usermodel.Paragraph;
import org.apache.poi.hwpf.usermodel.Picture;
import org.apache.poi.hwpf.usermodel.Range;
import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.openxml4j.opc.PackagePart;
import org.apache.poi.poifs.filesystem.DirectoryEntry;
import org.apache.poi.poifs.filesystem.DirectoryNode;
import org.apache.poi.poifs.filesystem.DocumentEntry;
import org.apache.poi.poifs.filesystem.DocumentInputStream;
import org.apache.poi.poifs.filesystem.Entry;
import org.apache.poi.poifs.filesystem.Ole10Native;
import org.apache.poi.poifs.filesystem.Ole10NativeException;
import org.apache.poi.poifs.filesystem.POIFSFileSystem;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFPicture;
import org.apache.poi.xwpf.usermodel.XWPFRun;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTObject;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * 魔数查询：https://www.cnblogs.com/bravexiaoxi/articles/File_Header_Footer.html
 * 
 * @author 006006
 *
 */
public class DocFileChecker {

	private static final Logger log = Logger.getLogger(DocFileChecker.class.getName());

	/**
	 * doc的格式文档
	 */
	private static final Integer DOC_VERSION_1 = 1;

	/**
	 * docx的格式文档
	 */
	private static final Integer DOC_VERSION_2 = 2;

	/**
	 * 存在ole的嵌入对象
	 */
	private static final String DOC_OLD_OBJECT = "Ole10Native";

	/**
	 * 存在 Package对象
	 */
	private static final String DOC_OLD_Package = "Package";

	/**
	 * 判断类型
	 */
	public final static String VIDEO = "video";
	public final static String AUDIO = "audio";
	private final static String OTHER = "other";
	public final static String TEXT = "text";
	public final static String IMAGE = "image";

	public static DocCheckResult checkFileType(String urlPath) {
		byte[] docMagicNumber = { (byte) 0xD0, (byte) 0xCF, (byte) 0x11, (byte) 0xE0, (byte) 0xA1, (byte) 0xB1,
				(byte) 0x1A, (byte) 0xE1 };
		byte[] docxMagicNumber = { (byte) 0x50, (byte) 0x4B, (byte) 0x03, (byte) 0x04 };

		Boolean bool = false;
		OPCPackage opcPackage;
		int docVersion = 2;

		/**
		 * BufferedInputStream 支持reset mark 和 reset 是一对儿
		 */
		try (InputStream fis = new BufferedInputStream(new URL(urlPath).openStream())) {

			fis.mark(8);
			byte[] header = new byte[8];
			int bytesRead = fis.read(header);

			if (bytesRead != header.length) {
				return DocCheckResult.CHECK_FILE_UNKNOWN_FILE_TYPE;
			}
			if (compareByteDoc(header, docMagicNumber)) {
				bool = true;
				docVersion = DOC_VERSION_1;
				log.log(Level.INFO, ".doc format document");
			} else if (compareByteDocx(header, docxMagicNumber)) {
				bool = true;
				docVersion = DOC_VERSION_2;
				log.log(Level.INFO, ".docx format document");
			} else {
				return DocCheckResult.CHECK_FILE_UNSUPPORT_FILE;
			}

			if (bool) {
				// 重新读取信息
				fis.reset();

				if (docVersion == DOC_VERSION_1) {
					
					getDocExtractorByPOI(new BufferedInputStream(new URL(urlPath).openStream()));
					
					boolean boolDoc = getWordExtratorByPOIFSFileSystem(fis);
					if (boolDoc) {
						return DocCheckResult.CHECK_FILE_ILLEGE_FILE;
					}
				}
				if (docVersion == DOC_VERSION_2) {
					
					
					getDocxExtractorByPOI(new BufferedInputStream(new URL(urlPath).openStream()));
					
					opcPackage = OPCPackage.open(fis);
					PackagePart part = opcPackage.getParts().stream()
							.filter(item -> item.getPartName().getName().equals("/word/document.xml")).findFirst()
							.orElse(null);
					InputStream wordRelsInputStream = part.getInputStream();
					DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
					DocumentBuilder builder = factory.newDocumentBuilder();
					Document doc = builder.parse(wordRelsInputStream);

					String content = convertDocumentToString(doc);
					if (content.indexOf("ProgID=\"Package\"") > 0) {
						return DocCheckResult.CHECK_FILE_ILLEGE_FILE;
					}
				}
				return DocCheckResult.CHECK_FILE_SUCCESS;
			} else {
				return DocCheckResult.CHECK_FILE_ERROR;
			}

		} catch (Exception e) {
			log.log(Level.SEVERE, e.getMessage());
			return DocCheckResult.CHECK_FILE_ERROR;
		}
	}

	/**
	 * 获取doc文件中，是否包含ole对象。
	 * @param fis
	 * @return
	 * @throws IOException
	 */
	private static boolean getWordExtratorByPOIFSFileSystem(InputStream fis) throws IOException {
		try (POIFSFileSystem poifs = new POIFSFileSystem(fis);) {
			DirectoryEntry root = poifs.getRoot();

			// 遍历所有 OLE 对象
			for (Entry entry : root) {
				log.log(Level.INFO, "Entry Name: " + entry.getName());

				// 如果是目录（可能包含嵌套的 OLE 对象）
				if (entry.isDirectoryEntry()) {
					DirectoryEntry dir = (DirectoryEntry) entry;
					boolean childContains = checkChild(dir, "--");
					if (childContains) {
						return childContains;
					}
				}
				// 如果是文件（可能是嵌入的 OLE 数据）
				else if (entry.isDocumentEntry()) {
					DocumentEntry docEntry = (DocumentEntry) entry;

					// 检查是否是已知的 OLE 对象类型
					boolean contains = isLikelyOleObject(docEntry);
					if (contains) {
						return contains;
					}
				}
			}
		}
		return false;
	}

	/**
	 * 递归判断，文件是目录还是对象，如果是目录，进一层
	 * @param child
	 * @return
	 */
	private static boolean checkChild(DirectoryEntry dir, String stuff) {
		for (Entry child : dir) {
			if (child.isDirectoryEntry()) {
				stuff = stuff + " ---|";
				DirectoryEntry dirEntry = (DirectoryEntry) child;
				return checkChild(dirEntry, stuff);

			} else {
				String name = child.getName();
				/**
				 * 音频、视频等可执行文件 
				 */
				if (name.contains(DOC_OLD_OBJECT)) {

					DocumentEntry docEntry = (DocumentEntry) child;
					try (DocumentInputStream dis = new DocumentInputStream(docEntry)) {

						byte[] header = new byte[8];
						dis.read(header);

						log.log(Level.INFO, "OLE 对象:" + child.getName() +",文件头:" + bytesToHex(header) + "可能是: " + identifyByHeader(header));
						
					} catch (Exception e) {
					}

					return true;
				}
				
				/**
				 * 非old对象，可能时 ppt，doc，xls等
				 */
				if (name.contains(DOC_OLD_Package)) {
					DocumentEntry docEntry = (DocumentEntry) child;
					try (DocumentInputStream dis = new DocumentInputStream(docEntry)) {

						byte[] header = new byte[8];
						dis.read(header);
						log.log(Level.INFO, "OLE 对象:" + child.getName() +",文件头:" + bytesToHex(header) + "可能是: " + identifyByHeader(header));
					} catch (Exception e) {
						System.out.println(e);
					}

					return true;
				}
			}
		}
		return false;
	}

	/**
	 * 判断文件类型
	 * 
	 * @param header
	 * @return
	 */
	private static String identifyByHeader(byte[] header) {
		// AVI 文件
		if (header[0] == 'R' && header[1] == 'I' && header[2] == 'F' && header[3] == 'F') {
			return "AVI 视频";
		}
		// Excel 文件
		if (header[0] == (byte) 0xD0 && header[1] == (byte) 0xCF && header[2] == (byte) 0x11
				&& header[3] == (byte) 0xE0) {
			return "Excel 文档";
		}
		// Word 文档
		if (header[0] == (byte) 0xEC && header[1] == (byte) 0xA5 && header[2] == (byte) 0xC1
				&& header[3] == (byte) 0x00) {
			return "Word 文档";
		}
		// MP4 文件
		if (header[4] == 'f' && header[5] == 't' && header[6] == 'y' && header[7] == 'p') {
			return "MP4 视频";
		}
		return "未知类型";
	}

	/**
	 * 转十六进制
	 * 
	 * @param bytes
	 * @return
	 */
	private static String bytesToHex(byte[] bytes) {
		StringBuilder sb = new StringBuilder();
		for (byte b : bytes) {
			sb.append(String.format("%02X ", b));
		}
		return sb.toString().trim();
	}

	@SuppressWarnings("unused")
	@Deprecated
	private static void getDocExtractorByHWPFDocument(InputStream fis) throws IOException {
		try (HWPFDocument document = new HWPFDocument(fis)) {
			WordExtractor extractor = new WordExtractor(document);

			// 获取所有嵌入的 OLE 对象
			DirectoryEntry root = extractor.getRoot();

			for (Entry entry : root) {
				// 如果是目录（可能包含嵌套的 OLE 对象）
				if (entry.isDirectoryEntry()) {
					DirectoryEntry dir = (DirectoryEntry) entry;
					// 遍历子项
					for (Entry child : dir) {
						if (child.getName().toLowerCase().contains("ole")) {
							System.out.println("    (Possible OLE Object)");
						}
					}
				}
				// 如果是文件（可能是嵌入的 OLE 数据）
				else if (entry.isDocumentEntry()) {
					DocumentEntry docEntry = (DocumentEntry) entry;
					// 检查是否是已知的 OLE 对象类型
					if (isLikelyOleObject(docEntry)) {
					}
				}
			}
		}
	}

	/**
	 * doc的判断方式
	 * @param is
	 */
	private static void getDocExtractorByPOI(InputStream is) {
		try (HWPFDocument document = new HWPFDocument(is)) {
			PicturesTable picturesTable = document.getPicturesTable();
			// 处理文字
			Range range = document.getRange();
			for (int i = 0; i < range.numParagraphs(); i++) {
				Paragraph paragraph = range.getParagraph(i);
				// 处理图片
				for (int j = 0; j < paragraph.numCharacterRuns(); j++) {
					CharacterRun run = paragraph.getCharacterRun(j);
					// 判断是否含有嵌入对象
					int picOffset = run.getPicOffset();
					if (run.isOle2()) {
						OfficeEmbed officeEmbed = getOle2Result(document, picOffset);
						if (officeEmbed != null) {
//							byte[] b = officeEmbed.getB();
							// 获取后缀
							String suffix = officeEmbed.getStuff();
							log.log(Level.WARNING, "ole的后缀为：" + suffix);
						}
					}
					// 获取图片
					if (picOffset >= 0) {
						Picture picture = picturesTable.extractPicture(run, true);
						if (picture != null) {
//							byte[] b = picture.getContent();
							String type = picture.getMimeType();
							// 后缀
							String suffix = type.substring(type.lastIndexOf('/') + 1);
							if (!"emf".equals(suffix) && !"x-emf".equals(suffix)) {

							}
						}
					}
				}
			}
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	/**
	 * docx的处理方法
	 * 
	 * @param is
	 * @throws IOException
	 */
	private static void getDocxExtractorByPOI(InputStream is) throws IOException {
		try (XWPFDocument document = new XWPFDocument(is)) {
			List<XWPFParagraph> paragraphs = document.getParagraphs();
			for (XWPFParagraph para : paragraphs) {
				// 获取段落文本
				String text = para.getText();
				// 获取图片和文件
				for (XWPFRun run : para.getRuns()) {
					// 获取此段落所有嵌入图像
					List<XWPFPicture> xwpfPictures = run.getEmbeddedPictures();
					for (XWPFPicture item : xwpfPictures) {
						// 字节
						byte[] b = item.getPictureData().getData();
						// 类型
						String type = item.getPictureData().getPackagePart().getContentType();
						// 后缀
						String suffix = type.substring(type.lastIndexOf('/') + 1);
					}
					// 嵌入文件
					List<CTObject> c = run.getCTR().getObjectList();
					for (CTObject item : c) {
						NodeList nn = item.getDomNode().getChildNodes();
						for (int j = 0; j < nn.getLength(); j++) {
							Node node = nn.item(j);
							if (node != null) {
								String s = node.getNodeName();
								if ("o:OLEObject".equals(s)) {
									NamedNodeMap namedNodeMap = node.getAttributes();
									String rId = namedNodeMap.getNamedItem("r:id").getNodeValue();
									PackagePart packagePart = document.getPartById(rId);
									OfficeEmbed officeEmbed = OfficeUtils.getEmbedInfo(packagePart.getInputStream());
									if (officeEmbed != null) {
										String type = officeEmbed.getType();
										if (!OTHER.equals(type)) {
											byte[] b = officeEmbed.getB();
											// 获取后缀
											String suffix = officeEmbed.getStuff();
											log.log(Level.WARNING, "ole的后缀为：" + suffix);
										}
									}
								}
							}
						}
					}
				}
			}
		}
	}

	/**
	 * 根据objId获取嵌入文件
	 * 
	 * @param doc   doc文件
	 * @param objId 对象id
	 * @return 嵌入对象
	 * @throws IOException 
	 */
	private static OfficeEmbed getOle2Result(HWPFDocument doc, int objId) throws IOException {
		Entry entry = doc.getObjectsPool().getObjectById("_" + objId);
		if (entry == null) {
			return null;
		}
		if (entry.isDirectoryEntry()) {
			DirectoryNode dn = (DirectoryNode) entry;
			//package没有空格
			if (dn.hasEntry("Package")) {
				DocumentEntry packageEntry = (DocumentEntry) dn.getEntry("Package");
				try (DocumentInputStream dis = new DocumentInputStream(packageEntry)) {
					byte[] header = new byte[8];
					dis.read(header);
					log.log(Level.INFO, "Package 对象:" + packageEntry.getName() +",文件头:" + bytesToHex(header) + "可能是: " + identifyByHeader(header));

				}
				
			} else if (dn.hasEntry("Ole10Native")) { //视频文件有空格
				OfficeEmbed officeEmbed = OfficeUtils.getEmbedInfo(dn);
				if (officeEmbed != null) {
					String type = officeEmbed.getType();
					if (!OTHER.equals(type)) {
						return officeEmbed;
					}
				}
			}
		}
		return null;
	}

	/**
	 * 检查是否是可能的 OLE 对象（如视频、Excel、PPT 等）
	 */
	private static boolean isLikelyOleObject(DocumentEntry entry) {
		String name = entry.getName().toLowerCase();
		return name.contains("ole") || name.endsWith(".avi") || name.endsWith(".xls") || name.endsWith(".ppt");
	}

	/**
	 * 判断doc文档 需要判断8个长度
	 * 
	 * @param array1
	 * @param array2
	 * @return
	 */
	private static boolean compareByteDoc(byte[] array1, byte[] array2) {
		if (array1.length != array2.length) {
			return false;
		}
		for (int i = 0; i < array1.length; i++) {
			if (array1[i] != array2[i]) {
				return false;
			}
		}
		return true;
	}

	/**
	 * 判断docx文档 需要判断4个长度
	 * 
	 * @param array1
	 * @param array2
	 * @return
	 */
	private static boolean compareByteDocx(byte[] array1, byte[] array2) {
		byte[] copy = Arrays.copyOf(array1, 4);
		if (copy.length != array2.length) {
			return false;
		}
		for (int i = 0; i < copy.length; i++) {
			if (copy[i] != array2[i]) {
				return false;
			}
		}
		return true;
	}

	/**
	 * 将 Document 转换为 XML 字符串
	 * 
	 * @param doc
	 * @return
	 * @throws TransformerException
	 */
	public static String convertDocumentToString(Document doc) throws TransformerException {
		TransformerFactory tf = TransformerFactory.newInstance();
		Transformer transformer = tf.newTransformer();

		// 设置输出格式（缩进、编码等）
		transformer.setOutputProperty(OutputKeys.INDENT, "yes");
		transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
		transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");

		DOMSource source = new DOMSource(doc);
		StringWriter writer = new StringWriter();
		StreamResult result = new StreamResult(writer);

		transformer.transform(source, result);

		return writer.toString();
	}

	public static void main(String[] args) {

		// 10MB文件
		String urlPath = "https://file.ewt360.com/file/1985990335054742042";

		// docx 中含视频的 1MB文件
//		urlPath = "http://file.ewt360.com/file/2021429518963417312";

		// docx中 含小于1mb的视频内容
//		urlPath = "http://file.ewt360.com/file/2021437533372399987";

		// wps- docx文档
//		urlPath = "http://file.ewt360.com/file/2021443657995764242";

//		// doc文件中，含视频
//		urlPath = "http://file.ewt360.com/file/2021452995254657548";

//		//真实的2003的 doc格式文档  无法解压
//		urlPath = "http://file.ewt360.com/file/2021495395171819621";
//		
		// 2003文档，其中含word文件的数据 (2019未编辑过)
//		urlPath = "http://file.ewt360.com/file/2021500866960146775";
//		//2019编辑保存过
//		urlPath = "http://file.ewt360.com/file/2021522547955073550";

//		//物理文件+公式图片
//		urlPath = "http://file.ewt360.com/file/2021579585120772748";

//		//文件含MP3
//		urlPath = "http://file.ewt360.com/file/2021979712863978267";

		
		urlPath = "https://file.test.ewt360.com/api/fileService/file/2050244823914570316";
		
		Long time1 = System.currentTimeMillis();
		DocCheckResult res = checkFileType(urlPath);
		System.out.println("The file type:" + res.getType() + ", the msg" + res.getMsg());

		Long time2 = System.currentTimeMillis();
		System.out.println("cost time:" + (time2 - time1));
	}
}
